<!doctype html>
<html lang="en-US">
  <head>
    <link href="/assets/index.css" rel="stylesheet" type="text/css" />
    <script crossorigin="anonymous" src="/test-harness.js"></script>
    <script crossorigin="anonymous" src="/test-page-object.js"></script>
    <script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
  </head>
  <body>
    <main id="webchat"></main>
    <script type="importmap">
      {
        "imports": {
          "@testduet/wait-for": "https://unpkg.com/@testduet/wait-for@main/dist/wait-for.mjs",
          "jest-mock": "https://esm.sh/jest-mock",
          "react-dictate-button/internal": "https://unpkg.com/react-dictate-button@main/dist/react-dictate-button.internal.mjs"
        }
      }
    </script>
    <script type="module">
      import { waitFor } from '@testduet/wait-for';
      import { fn, spyOn } from 'jest-mock';
      import {
        SpeechGrammarList,
        SpeechRecognition,
        SpeechRecognitionAlternative,
        SpeechRecognitionErrorEvent,
        SpeechRecognitionEvent,
        SpeechRecognitionResult,
        SpeechRecognitionResultList
      } from 'react-dictate-button/internal';
      import { SpeechSynthesis, SpeechSynthesisEvent, SpeechSynthesisUtterance } from '../js/index.js';

      const {
        testHelpers: { createDirectLineEmulator },
        WebChat: { renderWebChat, testIds }
      } = window;

      run(async function () {
        const speechSynthesis = new SpeechSynthesis();
        const ponyfill = {
          SpeechGrammarList,
          SpeechRecognition: fn().mockImplementation(() => {
            const speechRecognition = new SpeechRecognition();

            spyOn(speechRecognition, 'abort');
            spyOn(speechRecognition, 'start');

            return speechRecognition;
          }),
          speechSynthesis,
          SpeechSynthesisUtterance
        };

        spyOn(speechSynthesis, 'cancel');
        spyOn(speechSynthesis, 'speak');

        const { directLine, store } = createDirectLineEmulator();

        renderWebChat(
          {
            directLine,
            store,
            styleOptions: { speechRecognitionContinuous: true },
            webSpeechPonyfillFactory: () => ponyfill
          },
          document.getElementById('webchat')
        );

        await pageConditions.uiConnected();

        // WHEN: Microphone button is clicked.
        await pageObjects.clickMicrophoneButton();

        // THEN: Should call SpeechSynthesis.speak() for fulfilling user gesture requirement for speech synthesis by synthesizing an empty utterance.
        await waitFor(() => expect(speechSynthesis.speak).toHaveBeenCalledTimes(1));

        expect(speechSynthesis.speak).toHaveBeenLastCalledWith(expect.any(SpeechSynthesisUtterance));
        expect(speechSynthesis.speak).toHaveBeenLastCalledWith(expect.objectContaining({ text: '' }));

        // WHEN: Synthesized the empty utterance.
        speechSynthesis.speak.mock.calls[0][0].dispatchEvent(
          new SpeechSynthesisEvent('end', { utterance: speechSynthesis.speak.mock.calls[0] })
        );

        // THEN: Should have stopped synthesis.
        expect(speechSynthesis).toHaveProperty('paused', false);
        expect(speechSynthesis).toHaveProperty('pending', false);
        expect(speechSynthesis).toHaveProperty('speaking', false);

        // AFTER: Microphone button is clicked.
        // THEN: Should construct a SpeechRecognition() instance.
        await waitFor(() => expect(ponyfill.SpeechRecognition).toHaveBeenCalledTimes(1));

        const { value: speechRecognition1 } = ponyfill.SpeechRecognition.mock.results[0];

        // THEN: Should call SpeechRecognition.start().
        expect(speechRecognition1.start).toHaveBeenCalledTimes(1);
        expect(speechRecognition1).toHaveProperty('continuous', true);

        // THEN: Send box should say "Listening…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Starting…'
        );

        // WHEN: Recognition started and interims result is dispatched.
        speechRecognition1.dispatchEvent(new Event('start'));
        speechRecognition1.dispatchEvent(new Event('audiostart'));
        speechRecognition1.dispatchEvent(new Event('soundstart'));
        speechRecognition1.dispatchEvent(new Event('speechstart'));

        // THEN: Send box should say "Listening…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Listening…'
        );

        // WHEN: Recognized interims of "Hello".
        speechRecognition1.dispatchEvent(
          new SpeechRecognitionEvent('result', {
            results: new SpeechRecognitionResultList(
              new SpeechRecognitionResult(new SpeechRecognitionAlternative(0, 'Hello'))
            )
          })
        );

        // THEN: Should display "Hello" in the send box.
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          expect.stringMatching(/^Hello\s/u)
        );

        // WHEN: Completed recognizing "Hello, World!".
        await (
          await directLine.actPostActivity(async () => {
            speechRecognition1.dispatchEvent(
              new SpeechRecognitionEvent('result', {
                results: new SpeechRecognitionResultList(
                  new SpeechRecognitionResult(new SpeechRecognitionAlternative(0, 'Hello, World!'))
                )
              })
            );
            speechRecognition1.dispatchEvent(
              new SpeechRecognitionEvent('result', {
                results: new SpeechRecognitionResultList(
                  SpeechRecognitionResult.fromFinalized(new SpeechRecognitionAlternative(0.9, 'Hello, World!'))
                )
              })
            );
          })
        ).resolveAll();

        // THEN: Should have send the activity.
        await pageConditions.numActivitiesShown(1);
        expect(pageElements.activityContents()[0]).toHaveProperty('textContent', 'Hello, World!');

        // THEN: Send box should say "Listening…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Listening…'
        );

        // WHEN: Bot replied "Aloha!"
        await directLine.emulateIncomingActivity({
          // "expectingInput" should allow barge-in during speech synthesis.
          inputHint: 'expectingInput',
          text: 'Aloha!',
          type: 'message'
        });

        // THEN: Should show the reply "Aloha!"
        await pageConditions.numActivitiesShown(2);
        expect(pageElements.activityContents()[1]).toHaveProperty('textContent', 'Aloha!');

        // THEN: Should call SpeechSynthesis.speak() again with utterance of "Aloha!"
        await waitFor(() => expect(speechSynthesis.speak).toHaveBeenCalledTimes(2));
        expect(speechSynthesis.speak).toHaveBeenLastCalledWith(expect.any(SpeechSynthesisUtterance));
        expect(speechSynthesis.speak).toHaveBeenLastCalledWith(expect.objectContaining({ text: 'Aloha!' }));

        expect(speechSynthesis).toHaveProperty('paused', false);
        expect(speechSynthesis).toHaveProperty('pending', false);
        expect(speechSynthesis).toHaveProperty('speaking', true);

        // THEN: Send box should continue to show "Listening…" as barge-in is allowed through "expectingInput".
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Listening…'
        );

        // WHEN: Barge-in with interims of "Good".
        speechRecognition1.dispatchEvent(
          new SpeechRecognitionEvent('result', {
            results: new SpeechRecognitionResultList(
              new SpeechRecognitionResult(new SpeechRecognitionAlternative(0, 'Good'))
            )
          })
        );

        // THEN: Should cancel speech synthesis.
        expect(speechSynthesis.cancel).toHaveBeenCalledTimes(1);
        expect(speechSynthesis).toHaveProperty('paused', false);
        expect(speechSynthesis).toHaveProperty('pending', false);
        expect(speechSynthesis).toHaveProperty('speaking', false);

        // WHEN: Completed recognition of "Good morning!"
        await (
          await directLine.actPostActivity(async () => {
            const { value: speechRecognition1 } = ponyfill.SpeechRecognition.mock.results[0];

            // WHEN: Interims are dispatched.
            speechRecognition1.dispatchEvent(
              new SpeechRecognitionEvent('result', {
                results: new SpeechRecognitionResultList(
                  SpeechRecognitionResult.fromFinalized(new SpeechRecognitionAlternative(0.9, 'Good morning!'))
                )
              })
            );
          })
        ).resolveAll();

        // THEN: Should have send the activity.
        await pageConditions.numActivitiesShown(3);
        expect(pageElements.activityContents()[2]).toHaveProperty('textContent', 'Good morning!');

        // THEN: Should not have called SpeechRecognition.abort().
        expect(speechRecognition1.abort).toHaveBeenCalledTimes(0);

        // WHEN: Click on the microphone button to stop recognition.
        await pageObjects.clickMicrophoneButton();

        // THEN: Should have called SpeechRecognition.abort() once.
        expect(speechRecognition1.abort).toHaveBeenCalledTimes(1);

        // WHEN: Speech recognition dispatch abort error event.
        speechRecognition1.dispatchEvent(new Event('speechend'));
        speechRecognition1.dispatchEvent(new Event('soundend'));
        speechRecognition1.dispatchEvent(new Event('audioend'));
        speechRecognition1.dispatchEvent(new SpeechRecognitionErrorEvent('error', { error: 'aborted' }));
        speechRecognition1.dispatchEvent(new Event('end'));

        // THEN: Should go back to text input mode.
        expect(pageElements.sendBoxTextBox()).toBeTruthy();

        // WHEN: Click ont he microphone button to start recognition again.
        await pageObjects.clickMicrophoneButton();

        // THEN: Should create a new instance of SpeechRecognition.
        expect(ponyfill.SpeechRecognition).toHaveBeenCalledTimes(2);

        const { value: speechRecognition2 } = ponyfill.SpeechRecognition.mock.results[1];

        // THEN: Should call SpeechRecognition.start().
        expect(speechRecognition2.start).toHaveBeenCalledTimes(1);
        expect(speechRecognition2).toHaveProperty('continuous', true);

        // THEN: Send box should say "Starting…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Starting…'
        );

        // WHEN: Speech recognition started.
        speechRecognition2.dispatchEvent(new Event('start'));
        speechRecognition2.dispatchEvent(new Event('audiostart'));
        speechRecognition2.dispatchEvent(new Event('soundstart'));
        speechRecognition2.dispatchEvent(new Event('speechstart'));

        // THEN: Send box should say "Listening…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Listening…'
        );

        // WHEN: Recognized result of "What's the weather?".
        await (
          await directLine.actPostActivity(async () => {
            speechRecognition2.dispatchEvent(
              new SpeechRecognitionEvent('result', {
                results: new SpeechRecognitionResultList(
                  SpeechRecognitionResult.fromFinalized(new SpeechRecognitionAlternative(0, "What's the weather?"))
                )
              })
            );
          })
        ).resolveAll();

        // THEN: Should have send the activity.
        await pageConditions.numActivitiesShown(4);
        expect(pageElements.activityContents()[3]).toHaveProperty('textContent', "What's the weather?");

        // THEN: Send box should keep say "Listening…" and hide the original text box.
        expect(pageElements.sendBoxTextBox()).toBeFalsy();
        expect(document.querySelector(`[data-testid="${testIds.sendBoxSpeechBox}"]`)).toHaveProperty(
          'textContent',
          'Listening…'
        );

        // WHEN: Web Speech provider abruptly stopped recognition.
        speechRecognition2.dispatchEvent(new Event('speechend'));
        speechRecognition2.dispatchEvent(new Event('soundend'));
        speechRecognition2.dispatchEvent(new Event('audioend'));
        speechRecognition2.dispatchEvent(new Event('end'));

        // THEN: Should go back to text input mode.
        expect(pageElements.sendBoxTextBox()).toBeTruthy();
      });
    </script>
  </body>
</html>
